Svenska

Utforska grunderna i binära sökträd (BST) och lär dig hur du implementerar dem effektivt i JavaScript. Denna guide täcker BST-struktur, operationer och praktiska exempel för utvecklare världen över.

Binära sökträd: En omfattande implementeringsguide i JavaScript

Binära sökträd (BST) är en fundamental datastruktur inom datavetenskap, som används flitigt för effektiv sökning, sortering och hämtning av data. Deras hierarkiska struktur möjliggör logaritmisk tidskomplexitet för många operationer, vilket gör dem till ett kraftfullt verktyg för att hantera stora datamängder. Denna guide ger en omfattande översikt över BST och demonstrerar deras implementering i JavaScript, riktad till utvecklare världen över.

Förståelse för binära sökträd

Vad är ett binärt sökträd?

Ett binärt sökträd är en trädbaserad datastruktur där varje nod har högst två barn, kallade vänster barn och höger barn. Den viktigaste egenskapen hos ett BST är att för en given nod gäller:

Denna egenskap säkerställer att elementen i ett BST alltid är ordnade, vilket möjliggör effektiv sökning och hämtning.

Nyckelbegrepp

Implementering av ett binärt sökträd i JavaScript

Definiera Node-klassen

Först definierar vi en `Node`-klass för att representera varje nod i BST:t. Varje nod kommer att innehålla en `key` för att lagra datan och `left`- och `right`-pekare till sina barn.


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

Definiera BinarySearchTree-klassen

Därefter definierar vi `BinarySearchTree`-klassen. Denna klass kommer att innehålla rotnoden och metoder för att infoga, söka, ta bort och traversera trädet.


class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  // Metoder kommer att läggas till här
}

Insättning

`insert`-metoden lägger till en ny nod med den givna nyckeln till BST:t. Insättningsprocessen bibehåller BST-egenskapen genom att placera den nya noden på lämplig position i förhållande till befintliga noder.


insert(key) {
  const newNode = new Node(key);

  if (this.root === null) {
    this.root = newNode;
  } else {
    this.insertNode(this.root, newNode);
  }
}

insertNode(node, newNode) {
  if (newNode.key < node.key) {
    if (node.left === null) {
      node.left = newNode;
    } else {
      this.insertNode(node.left, newNode);
    }
  } else {
    if (node.right === null) {
      node.right = newNode;
    } else {
      this.insertNode(node.right, newNode);
    }
  }
}

Exempel: Infoga värden i BST:t


const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

Sökning

`search`-metoden kontrollerar om en nod med den givna nyckeln finns i BST:t. Den traverserar trädet, jämför nyckeln med den aktuella nodens nyckel och rör sig till vänster eller höger underträd därefter.


search(key) {
  return this.searchNode(this.root, key);
}

searchNode(node, key) {
  if (node === null) {
    return false;
  }

  if (key < node.key) {
    return this.searchNode(node.left, key);
  } else if (key > node.key) {
    return this.searchNode(node.right, key);
  } else {
    return true;
  }
}

Exempel: Söka efter ett värde i BST:t


console.log(bst.search(9));  // Utskrift: true
console.log(bst.search(2));  // Utskrift: false

Borttagning

`remove`-metoden tar bort en nod med den givna nyckeln från BST:t. Detta är den mest komplexa operationen eftersom den måste bibehålla BST-egenskapen när noden tas bort. Det finns tre fall att beakta:


remove(key) {
  this.root = this.removeNode(this.root, key);
}

removeNode(node, key) {
  if (node === null) {
    return null;
  }

  if (key < node.key) {
    node.left = this.removeNode(node.left, key);
    return node;
  } else if (key > node.key) {
    node.right = this.removeNode(node.right, key);
    return node;
  } else {
    // nyckeln är lika med nodens nyckel

    // fall 1 - en lövnod
    if (node.left === null && node.right === null) {
      node = null;
      return node;
    }

    // fall 2 - noden har endast 1 barn
    if (node.left === null) {
      node = node.right;
      return node;
    } else if (node.right === null) {
      node = node.left;
      return node;
    }

    // fall 3 - noden har 2 barn
    const aux = this.findMinNode(node.right);
    node.key = aux.key;
    node.right = this.removeNode(node.right, aux.key);
    return node;
  }
}

findMinNode(node) {
  let current = node;
  while (current != null && current.left != null) {
    current = current.left;
  }
  return current;
}

Exempel: Ta bort ett värde från BST:t


bst.remove(7);
console.log(bst.search(7)); // Utskrift: false

Trädgenomgång

Trädgenomgång (traversering) innebär att besöka varje nod i trädet i en specifik ordning. Det finns flera vanliga genomgångsmetoder:


inOrderTraverse(callback) {
  this.inOrderTraverseNode(this.root, callback);
}

inOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.inOrderTraverseNode(node.left, callback);
    callback(node.key);
    this.inOrderTraverseNode(node.right, callback);
  }
}

preOrderTraverse(callback) {
  this.preOrderTraverseNode(this.root, callback);
}

preOrderTraverseNode(node, callback) {
  if (node !== null) {
    callback(node.key);
    this.preOrderTraverseNode(node.left, callback);
    this.preOrderTraverseNode(node.right, callback);
  }
}

postOrderTraverse(callback) {
  this.postOrderTraverseNode(this.root, callback);
}

postOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.postOrderTraverseNode(node.left, callback);
    this.postOrderTraverseNode(node.right, callback);
    callback(node.key);
  }
}

Exempel: Genomgång av BST:t


const printNode = (value) => console.log(value);

bst.inOrderTraverse(printNode);   // Utskrift: 3 5 8 9 10 11 12 13 14 15 18 20 25
bst.preOrderTraverse(printNode);  // Utskrift: 11 5 3 9 8 10 15 13 12 14 20 18 25
bst.postOrderTraverse(printNode); // Utskrift: 3 8 10 9 12 14 13 18 25 20 15 11

Minsta och största värden

Att hitta minsta och största värdena i ett BST är enkelt tack vare dess ordnade natur.


min() {
  return this.minNode(this.root);
}

minNode(node) {
  let current = node;
  while (current !== null && current.left !== null) {
    current = current.left;
  }
  return current;
}

max() {
  return this.maxNode(this.root);
}

maxNode(node) {
  let current = node;
  while (current !== null && current.right !== null) {
    current = current.right;
  }
  return current;
}

Exempel: Hitta minsta och största värden


console.log(bst.min().key); // Utskrift: 3
console.log(bst.max().key); // Utskrift: 25

Praktiska tillämpningar av binära sökträd

Binära sökträd används i en mängd olika tillämpningar, inklusive:

Prestandaöverväganden

Prestandan hos ett BST beror på dess struktur. I bästa fall, ett balanserat BST, möjliggörs logaritmisk tidskomplexitet för insättnings-, sök- och borttagningsoperationer. Men i värsta fall (t.ex. ett skevt träd) kan tidskomplexiteten försämras till linjär tid.

Balanserade vs. obalanserade träd

Ett balanserat BST är ett där höjdskillnaden mellan vänster och höger underträd för varje nod är högst ett. Självbalanserande algoritmer, såsom AVL-träd och röd-svarta träd, säkerställer att trädet förblir balanserat och ger konsekvent prestanda. Olika regioner kan kräva olika optimeringsnivåer baserat på belastningen på servern; balansering hjälper till att upprätthålla prestanda under hög global användning.

Tidskomplexitet

Avancerade BST-koncept

Självbalanserande träd

Självbalanserande träd är BST som automatiskt justerar sin struktur för att bibehålla balansen. Detta säkerställer att trädets höjd förblir logaritmisk, vilket ger konsekvent prestanda för alla operationer. Vanliga självbalanserande träd inkluderar AVL-träd och röd-svarta träd.

AVL-träd

AVL-träd upprätthåller balansen genom att säkerställa att höjdskillnaden mellan vänster och höger underträd för varje nod är högst ett. När denna balans störs utförs rotationer för att återställa balansen.

Röd-svarta träd

Röd-svarta träd använder färgegenskaper (röd eller svart) för att bibehålla balansen. De är mer komplexa än AVL-träd men erbjuder bättre prestanda i vissa scenarier.

JavaScript-kodexempel: Komplett implementering av binärt sökträd


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  insert(key) {
    const newNode = new Node(key);

    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
  }

  insertNode(node, newNode) {
    if (newNode.key < node.key) {
      if (node.left === null) {
        node.left = newNode;
      } else {
        this.insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null) {
        node.right = newNode;
      } else {
        this.insertNode(node.right, newNode);
      }
    }
  }

  search(key) {
    return this.searchNode(this.root, key);
  }

  searchNode(node, key) {
    if (node === null) {
      return false;
    }

    if (key < node.key) {
      return this.searchNode(node.left, key);
    } else if (key > node.key) {
      return this.searchNode(node.right, key);
    } else {
      return true;
    }
  }

  remove(key) {
    this.root = this.removeNode(this.root, key);
  }

  removeNode(node, key) {
    if (node === null) {
      return null;
    }

    if (key < node.key) {
      node.left = this.removeNode(node.left, key);
      return node;
    } else if (key > node.key) {
      node.right = this.removeNode(node.right, key);
      return node;
    } else {
      // nyckeln är lika med nodens nyckel

      // fall 1 - en lövnod
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      }

      // fall 2 - noden har endast 1 barn
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }

      // fall 3 - noden har 2 barn
      const aux = this.findMinNode(node.right);
      node.key = aux.key;
      node.right = this.removeNode(node.right, aux.key);
      return node;
    }
  }

  findMinNode(node) {
    let current = node;
    while (current != null && current.left != null) {
      current = current.left;
    }
    return current;
  }

  min() {
    return this.minNode(this.root);
  }

  minNode(node) {
    let current = node;
    while (current !== null && current.left !== null) {
      current = current.left;
    }
    return current;
  }

  max() {
    return this.maxNode(this.root);
  }

  maxNode(node) {
    let current = node;
    while (current !== null && current.right !== null) {
      current = current.right;
    }
    return current;
  }

  inOrderTraverse(callback) {
    this.inOrderTraverseNode(this.root, callback);
  }

  inOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.inOrderTraverseNode(node.left, callback);
      callback(node.key);
      this.inOrderTraverseNode(node.right, callback);
    }
  }

  preOrderTraverse(callback) {
    this.preOrderTraverseNode(this.root, callback);
  }

  preOrderTraverseNode(node, callback) {
    if (node !== null) {
      callback(node.key);
      this.preOrderTraverseNode(node.left, callback);
      this.preOrderTraverseNode(node.right, callback);
    }
  }

  postOrderTraverse(callback) {
    this.postOrderTraverseNode(this.root, callback);
  }

  postOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.postOrderTraverseNode(node.left, callback);
      this.postOrderTraverseNode(node.right, callback);
      callback(node.key);
    }
  }
}

// Exempel på användning
const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

const printNode = (value) => console.log(value);

console.log("In-order-genomgång:");
bst.inOrderTraverse(printNode);

console.log("Pre-order-genomgång:");
bst.preOrderTraverse(printNode);

console.log("Post-order-genomgång:");
bst.postOrderTraverse(printNode);

console.log("Minsta värde:", bst.min().key);
console.log("Största värde:", bst.max().key);

console.log("Sök efter 9:", bst.search(9));
console.log("Sök efter 2:", bst.search(2));

bst.remove(7);
console.log("Sök efter 7 efter borttagning:", bst.search(7));

Slutsats

Binära sökträd är en kraftfull och mångsidig datastruktur med många tillämpningar. Denna guide har gett en omfattande översikt över BST, som täcker deras struktur, operationer och implementering i JavaScript. Genom att förstå principerna och teknikerna som diskuteras i denna guide kan utvecklare världen över effektivt använda BST för att lösa ett brett spektrum av problem inom mjukvaruutveckling. Från att hantera globala databaser till att optimera sökalgoritmer är kunskapen om BST en ovärderlig tillgång för alla programmerare.

När du fortsätter din resa inom datavetenskap kommer utforskandet av avancerade koncept som självbalanserande träd och deras olika implementeringar att ytterligare förbättra din förståelse och dina färdigheter. Fortsätt att öva och experimentera med olika scenarier för att bemästra konsten att använda binära sökträd effektivt.